// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
 *******************************************************************************
 * Copyright (C) 2000-2010, International Business Machines Corporation and    *
 * others. All Rights Reserved.                                                *
 *******************************************************************************
 */

package com.ibm.icu.dev.tool.ime.indic;

import java.awt.event.InputMethodEvent;
import java.awt.event.KeyEvent;
import java.awt.font.TextAttribute;
import java.awt.font.TextHitInfo;
import java.awt.im.spi.InputMethodContext;
import java.text.AttributedCharacterIterator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

class IndicInputMethodImpl {

    protected char[] KBD_MAP;

    private static final char SUBSTITUTION_BASE = '\uff00';

    // Indexed by map value - SUBSTITUTION_BASE
    protected char[][] SUBSTITUTION_TABLE;

    // Invalid character.
    private static final char INVALID_CHAR = '\uffff';

    // Unmapped versions of some interesting characters.
    private static final char KEY_SIGN_VIRAMA = '\u0064'; // or just 'd'??
    private static final char KEY_SIGN_NUKTA = '\u005d'; // or just ']'??

    // Two succeeding viramas are replaced by one virama and one ZWNJ.
    // Viram followed by Nukta is replaced by one VIRAMA and one ZWJ
    private static final char ZWJ = '\u200d';
    private static final char ZWNJ = '\u200c';

    // Backspace
    private static final char BACKSPACE = '\u0008';

    // Sorted list of characters which can be followed by Nukta
    protected char[] JOIN_WITH_NUKTA;

    // Nukta form of the above characters
    protected char[] NUKTA_FORM;

    // private int log2;
    private int power;
    private int extra;

    // cached TextHitInfo. Only one type of TextHitInfo is required.
    private static final TextHitInfo ZERO_TRAILING_HIT_INFO = TextHitInfo.trailing(0);

    /**
     * Returns the index of the given character in the JOIN_WITH_NUKTA array. If character is not
     * found, -1 is returned.
     */
    private int nuktaIndex(char ch) {
        if (JOIN_WITH_NUKTA == null) {
            return -1;
        }

        int probe = power;
        int index = 0;

        if (JOIN_WITH_NUKTA[extra] <= ch) {
            index = extra;
        }

        while (probe > (1 << 0)) {
            probe >>= 1;

            if (JOIN_WITH_NUKTA[index + probe] <= ch) {
                index += probe;
            }
        }

        if (JOIN_WITH_NUKTA[index] != ch) {
            index = -1;
        }

        return index;
    }

    /**
     * Returns the equivalent character for hindi locale.
     *
     * @param originalChar The original character.
     */
    private char getMappedChar(char originalChar) {
        if (originalChar <= KBD_MAP.length) {
            return KBD_MAP[originalChar];
        }

        return originalChar;
    }

    // Array used to hold the text to be sent.
    // If the last character was not committed it is stored in text[0].
    // The variable totalChars give an indication of whether the last
    // character was committed or not. If at any time ( but not within a
    // a call to dispatchEvent ) totalChars is not equal to 0 ( it can
    // only be 1 otherwise ) the last character was not committed.
    private char[] text = new char[4];

    // this is always 0 before and after call to dispatchEvent. This character assumes
    // significance only within a call to dispatchEvent.
    private int committedChars = 0; // number of committed characters

    // the total valid characters in variable text currently.
    private int totalChars = 0; // number of total characters ( committed + composed )

    private boolean lastCharWasVirama = false;

    private InputMethodContext context;

    //
    // Finds the high bit by binary searching
    // through the bits in n.
    //
    private static byte highBit(int n) {
        if (n <= 0) {
            return -32;
        }

        byte bit = 0;

        if (n >= 1 << 16) {
            n >>= 16;
            bit += 16;
        }

        if (n >= 1 << 8) {
            n >>= 8;
            bit += 8;
        }

        if (n >= 1 << 4) {
            n >>= 4;
            bit += 4;
        }

        if (n >= 1 << 2) {
            n >>= 2;
            bit += 2;
        }

        if (n >= 1 << 1) {
            n >>= 1;
            bit += 1;
        }

        return bit;
    }

    IndicInputMethodImpl(
            char[] keyboardMap,
            char[] joinWithNukta,
            char[] nuktaForm,
            char[][] substitutionTable) {
        KBD_MAP = keyboardMap;
        JOIN_WITH_NUKTA = joinWithNukta;
        NUKTA_FORM = nuktaForm;
        SUBSTITUTION_TABLE = substitutionTable;

        if (JOIN_WITH_NUKTA != null) {
            int log2 = highBit(JOIN_WITH_NUKTA.length);

            power = 1 << log2;
            extra = JOIN_WITH_NUKTA.length - power;
        } else {
            power = extra = 0;
        }
    }

    void setInputMethodContext(InputMethodContext context) {
        this.context = context;
    }

    void handleKeyTyped(KeyEvent kevent) {
        char keyChar = kevent.getKeyChar();
        char currentChar = getMappedChar(keyChar);

        // The Explicit and Soft Halanta case.
        if (lastCharWasVirama) {
            switch (keyChar) {
                case KEY_SIGN_NUKTA:
                    currentChar = ZWJ;
                    break;
                case KEY_SIGN_VIRAMA:
                    currentChar = ZWNJ;
                    break;
                default:
            } // endSwitch
        } // endif

        if (currentChar == INVALID_CHAR) {
            kevent.consume();
            return;
        }

        if (currentChar == BACKSPACE) {
            lastCharWasVirama = false;

            if (totalChars > 0) {
                totalChars = committedChars = 0;
            } else {
                return;
            }
        } else if (keyChar == KEY_SIGN_NUKTA) {
            int nuktaIndex = nuktaIndex(text[0]);

            if (nuktaIndex != -1) {
                text[0] = NUKTA_FORM[nuktaIndex];
            } else {
                // the last character was committed, commit just Nukta.
                // Note : the lastChar must have been committed if it is not one of
                // the characters which combine with nukta.
                // the state must be totalChars = committedChars = 0;
                text[totalChars++] = currentChar;
            }

            committedChars += 1;
        } else {
            int nuktaIndex = nuktaIndex(currentChar);

            if (nuktaIndex != -1) {
                // Commit everything but currentChar
                text[totalChars++] = currentChar;
                committedChars = totalChars - 1;
            } else {
                if (currentChar >= SUBSTITUTION_BASE) {
                    char[] sub = SUBSTITUTION_TABLE[currentChar - SUBSTITUTION_BASE];

                    System.arraycopy(sub, 0, text, totalChars, sub.length);
                    totalChars += sub.length;
                } else {
                    text[totalChars++] = currentChar;
                }

                committedChars = totalChars;
            }
        }

        ACIText aText = new ACIText(text, 0, totalChars, committedChars);
        int composedCharLength = totalChars - committedChars;
        TextHitInfo caret = null, visiblePosition = null;
        switch (composedCharLength) {
            case 0:
                break;
            case 1:
                visiblePosition = caret = ZERO_TRAILING_HIT_INFO;
                break;
            default:
                // The code should not reach here. There is no case where there can be
                // more than one character pending.
        }

        context.dispatchInputMethodEvent(
                InputMethodEvent.INPUT_METHOD_TEXT_CHANGED,
                aText,
                committedChars,
                caret,
                visiblePosition);

        if (totalChars == 0) {
            text[0] = INVALID_CHAR;
        } else {
            text[0] = text[totalChars - 1]; // make text[0] hold the last character
        }

        lastCharWasVirama = keyChar == KEY_SIGN_VIRAMA && !lastCharWasVirama;

        totalChars -= committedChars;
        committedChars = 0;
        // state now text[0] = last character
        // totalChars = ( last character committed )? 0 : 1;
        // committedChars = 0;

        kevent.consume(); // prevent client from getting this event.
    }

    void endComposition() {
        if (totalChars != 0) { // if some character is not committed.
            ACIText aText = new ACIText(text, 0, totalChars, totalChars);
            context.dispatchInputMethodEvent(
                    InputMethodEvent.INPUT_METHOD_TEXT_CHANGED, aText, totalChars, null, null);
            totalChars = committedChars = 0;
            text[0] = INVALID_CHAR;
            lastCharWasVirama = false;
        }
    }

    // custom AttributedCharacterIterator -- much lightweight since currently there is no
    // attribute defined on the text being generated by the input method.
    private class ACIText implements AttributedCharacterIterator, Cloneable {
        private char[] text = null;
        private int committed = 0;
        private int index = 0;

        ACIText(char[] chArray, int offset, int length, int committed) {
            this.text = new char[length];
            this.committed = committed;
            System.arraycopy(chArray, offset, text, 0, length);
        }

        // CharacterIterator methods.
        public char first() {
            return _setIndex(0);
        }

        public char last() {
            if (text.length == 0) {
                return _setIndex(text.length);
            }
            return _setIndex(text.length - 1);
        }

        public char current() {
            if (index == text.length) return DONE;
            return text[index];
        }

        public char next() {
            if (index == text.length) {
                return DONE;
            }
            return _setIndex(index + 1);
        }

        public char previous() {
            if (index == 0) return DONE;
            return _setIndex(index - 1);
        }

        public char setIndex(int position) {
            if (position < 0 || position > text.length) {
                throw new IllegalArgumentException();
            }
            return _setIndex(position);
        }

        public int getBeginIndex() {
            return 0;
        }

        public int getEndIndex() {
            return text.length;
        }

        public int getIndex() {
            return index;
        }

        public ACIText clone() {
            try {
                return (ACIText) super.clone();
            } catch (CloneNotSupportedException e) {
                throw new IllegalStateException();
            }
        }

        // AttributedCharacterIterator methods.
        public int getRunStart() {
            return index >= committed ? committed : 0;
        }

        public int getRunStart(AttributedCharacterIterator.Attribute attribute) {
            return (index >= committed && attribute.equals(TextAttribute.INPUT_METHOD_UNDERLINE))
                    ? committed
                    : 0;
        }

        public int getRunStart(Set attributes) {
            return (index >= committed && attributes.contains(TextAttribute.INPUT_METHOD_UNDERLINE))
                    ? committed
                    : 0;
        }

        public int getRunLimit() {
            return index < committed ? committed : text.length;
        }

        public int getRunLimit(AttributedCharacterIterator.Attribute attribute) {
            return (index < committed && attribute.equals(TextAttribute.INPUT_METHOD_UNDERLINE))
                    ? committed
                    : text.length;
        }

        public int getRunLimit(Set attributes) {
            return (index < committed && attributes.contains(TextAttribute.INPUT_METHOD_UNDERLINE))
                    ? committed
                    : text.length;
        }

        public Map getAttributes() {
            HashMap result = new HashMap<>();
            if (index >= committed && committed < text.length) {
                result.put(
                        TextAttribute.INPUT_METHOD_UNDERLINE,
                        TextAttribute.UNDERLINE_LOW_ONE_PIXEL);
            }
            return result;
        }

        public Object getAttribute(AttributedCharacterIterator.Attribute attribute) {
            if (index >= committed
                    && committed < text.length
                    && attribute.equals(TextAttribute.INPUT_METHOD_UNDERLINE)) {

                return TextAttribute.UNDERLINE_LOW_ONE_PIXEL;
            }
            return null;
        }

        public Set getAllAttributeKeys() {
            HashSet result = new HashSet();
            if (committed < text.length) {
                result.add(TextAttribute.INPUT_METHOD_UNDERLINE);
            }
            return result;
        }

        // private methods

        /** This is always called with valid i ( 0 < i <= text.length ) */
        private char _setIndex(int i) {
            index = i;
            if (i == text.length) {
                return DONE;
            }
            return text[i];
        }
    }
}
